接續上一篇 【第二五天 - Flutter 知名外送平台畫面練習(上)】~~。
今日的程式碼 => GITHUB
我們建立好 FappBar 後。再 HomePage 來使用它。這裡將會介紹用到的套件。和整個 TabBarController 和 ScrollController 的互動方式
  scroll_to_index: ^2.0.0
  rect_getter: ^1.0.0
更多資訊請參考 => 官方文件
AutoScrollController scrollController = AutoScrollController();
AutoScrollTag(
  key: ValueKey(index),
  controller: controller,
  index: index,
  child: child
)
controller.scrollToIndex(index, preferPosition: AutoScrollPosition.begin)
globalKey。RectGetter 來觀測 childRect rect = RectGetter.getRectFromKey(globalKey); 來取得 rect資料參考來自 官方文件
// Import package
import 'package:rect_getter/rect_getter.dart';
// Instantiate it
var globalKey = RectGetter.createGlobalKey();
var rectGetter = new RectGetter(
    key: globalKey,
    child: _child,
);
or
var rectGetter = new RectGetter.defaultKey(
    child: _child,
);
// and add it to your layout .
// then you can get rect by
Rect rect = rectGetter.getRect();
or
Rect rect = RectGetter.getRectFromKey(globalKey);
大致講一下邏輯,和思路。
wholePage 的 RectGetter,用來關注整個畫面的大小NotificationListener 來管理 CustomScrollView 和 TabBar 的互動,換句話說,就是當點擊、聽直、滑動等...,一系列的 ScrollNotification 事件觸發時,我就要去計算我的畫面,然後坐我想要做的事情SliverScrollView 也就是包含 AppBar(SliverAppBar)、Body(SliverList)。FAppBar,可以參考前一篇 【第二五天 - Flutter 知名外送平台畫面練習(上)】。onCollapsed,來讓 AppBar 操作。SliverList,並且在 SliverChildListDelegate,裡面使用 List.generate,並在 List.generate 裡面回傳 item 樣式。AutoScrollTag 來達到 scroll_to_index。NotificationListener 的 onScrollNotification。getVisibleItemsIndex
class HomeScreen extends StatefulWidget {
  @override
  _HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen>
    with SingleTickerProviderStateMixin {
  /// 使否展開
  bool isCollapsed = false;
  late AutoScrollController scrollController;
  late TabController tabController;
  /// 展開高度
  final double expandedHeight = 500.0;
  /// 頁面資料
  final PageData data = ExampleData.data;
  /// 折疊高度
  final double collapsedHeight = kToolbarHeight;
  /// Instantiate RectGetter
  final wholePage = RectGetter.createGlobalKey();
  Map<int, dynamic> itemKeys = {};
  /// prevent animate when press on tab bar
  /// 避免當我們點擊 tab bar 時,動畫還在動,還在計算。
  bool pauseRectGetterIndex = false;
  @override
  void initState() {
    /// tabController 出使話
    tabController = TabController(length: data.categories.length, vsync: this);
    scrollController = AutoScrollController();
    super.initState();
  }
  @override
  void dispose() {
    scrollController.dispose();
    tabController.dispose();
    super.dispose();
  }
  /// 取得螢幕可看到的 index 有哪些
  List<int> getVisibleItemsIndex() {
    // get ListView Rect
    Rect? rect = RectGetter.getRectFromKey(wholePage);
    List<int> items = [];
    if (rect == null) return items;
    itemKeys.forEach((index, key) {
      Rect? itemRect = RectGetter.getRectFromKey(key);
      if (itemRect == null) return;
      // y 軸座越大,代表越下面
      // 如果 item 上方的座標 比 listView 的下方的座標 的位置的大 代表不在畫面中。
      // bottom meaning => The offset of the bottom edge of this widget from the y axis.
      // top meaning => The offset of the top edge of this widget from the y axis.
      if (itemRect.top > rect.bottom) return;
      // 如果 item 下方的座標 比 listView 的上方的座標 的位置的小 代表不在畫面中。
      if (itemRect.bottom < rect.top) return;
      items.add(index);
    });
    return items;
  }
  /// 用來傳遞給 appBar 的 function
  void onCollapsed(bool value) {
    if (this.isCollapsed == value) return;
    setState(() => this.isCollapsed = value);
  }
  /// true表示消費掉當前通知不再向上一级NotificationListener傳遞通知,false則會再向上一级NotificationListener傳遞通知;
  bool onScrollNotification(ScrollNotification notification) {
    // 不想讓上一層知道,無需做動作。
    if (pauseRectGetterIndex) return true;
    // 取得標籤的長度
    int lastTabIndex = tabController.length - 1;
    // 取得現在畫面上可以看得到的 Items Index
    List<int> visibleItems = getVisibleItemsIndex();
    bool reachLastTabIndex = visibleItems.isNotEmpty &&
        visibleItems.length <= 2 &&
        visibleItems.last == lastTabIndex;
    // 如果到達最後一個 index 就跳轉到最後一個 index
    if (reachLastTabIndex) {
      tabController.animateTo(lastTabIndex);
    } else {
      // 取得畫面中的 item 的中間值。例:2,3,4 中間的就是 3
      // 求一個數字列表的乘積
      int sumIndex = visibleItems.reduce((value, element) => value + element);
      // 5 ~/ 2 = 2  => Result is an int 取整數
      int middleIndex = sumIndex ~/ visibleItems.length;
      if (tabController.index != middleIndex)
        tabController.animateTo(middleIndex);
    }
    return false;
  }
  /// TabBar 的動畫。
  void animateAndScrollTo(int index) {
    pauseRectGetterIndex = true;
    tabController.animateTo(index);
    // Scroll 到 index 並使用 begin 的模式,結束後,把 pauseRectGetterIndex 設為 false 暫停執行 ScrollNotification
    scrollController
        .scrollToIndex(index, preferPosition: AutoScrollPosition.begin)
        .then((value) => pauseRectGetterIndex = false);
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      extendBodyBehindAppBar: true, //是否延伸body至顶部。
      backgroundColor: scheme.background,
      body: RectGetter(
        key: wholePage,
        /// NotificationListener 是一個由下往上傳遞通知,true 阻止通知、false 傳遞通知,確保指監聽滾動的通知
        /// ScrollNotification => https://www.jianshu.com/p/d80545454944
        child: NotificationListener<ScrollNotification>(
          child: buildSliverScrollView(),
          onNotification: onScrollNotification,
        ),
      ),
    );
  }
  /// CustomScrollView + SliverList + SliverAppBar
  Widget buildSliverScrollView() {
    return CustomScrollView(
      controller: scrollController,
      slivers: [
        buildAppBar(),
        buildBody(),
      ],
    );
  }
  /// AppBar
  SliverAppBar buildAppBar() {
    return FAppBar(
      data: data,
      context: context,
      expandedHeight: expandedHeight,
      // 期許展開的高度
      collapsedHeight: collapsedHeight,
      // 折疊高度
      isCollapsed: isCollapsed,
      onCollapsed: onCollapsed,
      tabController: tabController,
      onTap: (index) => animateAndScrollTo(index),
    );
  }
  /// Body
  SliverList buildBody() {
    return SliverList(
      delegate: SliverChildListDelegate(List.generate(
        data.categories.length,
        (index) {
          return buildCategoryItem(index);
        },
      )),
    );
  }
  /// ListItem
  Widget buildCategoryItem(int index) {
    // 建立 itemKeys 的 Key
    itemKeys[index] = RectGetter.createGlobalKey();
    Category category = data.categories[index];
    return RectGetter(
      // 傳GlobalKey,之後可以 RectGetter.getRectFromKey(key) 的方式獲得 Rect
      key: itemKeys[index],
      child: AutoScrollTag(
        key: ValueKey(index),
        index: index,
        controller: scrollController,
        child: CategorySection(category: category),
      ),
    );
  }
}